﻿#region Using Statements
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using System.Xml;
using Microsoft.Xna.Framework.Graphics;
using Primitives3D;
#endregion // Using Statements

namespace ParticleSystem
{
    public class DrawablePlane
    {
        #region Fields
        /// <summary>
        /// The plane associated with this object.
        /// </summary>
        private Plane mPlane;
        public Plane plane
        {
            get { return mPlane; }
        }

        /// <summary>
        /// Flag indicating whether or not this plane should be rendered.
        /// </summary>
        private bool mDraw;

        /// <summary>
        /// Color used to draw this plane.
        /// </summary>
        public Color color;

        /// <summary>
        /// The graphics device that will be used to render this plane.
        /// </summary>
        private GraphicsDevice mGraphicsDevice;

        /// <summary>
        /// Vertex buffer containing this plane's vertices.
        /// </summary>
        private VertexBuffer mVertexBuffer;

        /// <summary>
        /// Index buffer containing this plane's indices.
        /// </summary>
        private IndexBuffer mIndexBuffer;

        /// <summary>
        /// The effect used to render the plane.
        /// </summary>
        private BasicEffect mPrimitiveEffect;
        #endregion // Fields

        #region Constructors
        /// <summary>
        /// Constructor for a DrawablePlane.
        /// </summary>
        /// <param name="graphicsDevice">The graphics device used to draw this plane.</param>
        /// <param name="plane">The plane associated with this DrawablePlane.</param>
        /// <param name="draw">Whether or not this plane should be rendered (default to false).</param>
        public DrawablePlane(GraphicsDevice graphicsDevice, Plane plane, bool draw = false)
        {
            mGraphicsDevice = graphicsDevice;
            mPlane = plane;
            mDraw = draw;
            color = Color.SaddleBrown;

            Initialize();
        }

        // <summary>
        /// Create an axis aligned drawable plane from an XML element.
        /// </summary>
        /// <param name="planeElement">The XML element containing information about the drawable plane.</param>
        /// <returns>The plane created from the XML element.</returns>
        public static DrawablePlane AxisAlignedPlaneFromXml(XmlElement planeElement, GraphicsDevice graphicsDevice)
        {
            Vector3 normal = Vector3.Zero;
            float d = 0.0f;
            bool draw = false;

            foreach (XmlElement planeElementChild in planeElement.ChildNodes)
            {
                // found the plane axis
                if (planeElementChild.Name.Equals("axis"))
                {
                    String axisStr = planeElementChild.ChildNodes[0].Value;
                    if (axisStr.Equals("X"))
                    {
                        normal = Vector3.UnitX;
                    }
                    else if (axisStr.Equals("Y"))
                    {
                        normal = Vector3.UnitY;
                    }
                    else if (axisStr.Equals("Z"))
                    {
                        normal = Vector3.UnitZ;
                    }
                }
                // found the plane distance ("d") value
                else if (planeElementChild.Name.Equals("value"))
                {
                    d = float.Parse(planeElementChild.ChildNodes[0].Value);
                }
                // found the flag indicating whether or not this plane should be drawn
                else if (planeElementChild.Name.Equals("draw"))
                {
                    draw = bool.Parse(planeElementChild.ChildNodes[0].Value);
                }
            }

            // if the normal is still zero, the xml parsing failed
            if (normal.Equals(Vector3.Zero))
            {
                throw new Exception("Could not retrieve the drawable collision plane from XML!");
            }

            return new DrawablePlane(graphicsDevice, new Plane(normal, d), draw);
        }
        #endregion // Constructors

        #region Loading and Initialization
        /// <summary>
        /// Initialize all rendering member variables for this plane (if necessary).
        /// </summary>
        private void Initialize()
        {
            // only set up rendering member variables if we're going to draw this plane
            if (mDraw)
            {
                // create the effect we'll use to render this plane
                mPrimitiveEffect = new BasicEffect(mGraphicsDevice);
                mPrimitiveEffect.EnableDefaultLighting();
                mPrimitiveEffect.VertexColorEnabled = true;

                // this projects the cartesion origin onto our plane
                Vector3 planeOrigin = mPlane.D * mPlane.Normal;

                // pick another random point on the plane
                // if P0 is a point on the plane, N is the plane normal, and P is a random point,
                // then the projection of P onto the plane can be found by:
                //      Q = Projection of P-P0 onto N = Dot(P-P0,N) * N
                //      Projection of P onto Plane = P - Q
                // Keep looking for a random points until we find one that isn't equal to our plane origin.
                Vector3 planeRandPt;
                do
                {
                    Vector3 randPt = new Vector3(Utility.randomFloat(), Utility.randomFloat(), Utility.randomFloat());
                    planeRandPt = randPt - (Vector3.Dot(randPt - planeOrigin, mPlane.Normal) * mPlane.Normal);
                }
                while (planeOrigin == planeRandPt);

                // calcuate our 4 vertices that will define this plane
                Vector3 planarU = Vector3.Normalize(planeRandPt - planeOrigin);
                Vector3 planarV = Vector3.Normalize(Vector3.Cross(mPlane.Normal, planarU));
                float scale = 100;
                Vector3 point0 = planeOrigin + (planarU * (-scale) + planarV * (-scale));
                Vector3 point1 = planeOrigin + (planarU * (-scale) + planarV * scale);
                Vector3 point2 = planeOrigin + (planarU * scale + planarV * scale);
                Vector3 point3 = planeOrigin + (planarU * scale + planarV * (-scale));

                List<VertexPositionNormalColor> vertices = new List<VertexPositionNormalColor>();
                vertices.Add(new VertexPositionNormalColor(point0, mPlane.Normal, color));
                vertices.Add(new VertexPositionNormalColor(point1, mPlane.Normal, color));
                vertices.Add(new VertexPositionNormalColor(point2, mPlane.Normal, color));
                vertices.Add(new VertexPositionNormalColor(point3, mPlane.Normal, color));
                vertices.Add(new VertexPositionNormalColor(point0, -mPlane.Normal, color));
                vertices.Add(new VertexPositionNormalColor(point1, -mPlane.Normal, color));
                vertices.Add(new VertexPositionNormalColor(point2, -mPlane.Normal, color));
                vertices.Add(new VertexPositionNormalColor(point3, -mPlane.Normal, color));

                // create the vertex buffer and set its data
                mVertexBuffer = new VertexBuffer(mGraphicsDevice, typeof(VertexPositionNormalColor), 8, BufferUsage.None);
                mVertexBuffer.SetData(vertices.ToArray());

                // set up our index buffer for the vertices we've just created
                // recall that all triangles are drawn with indices in counter-clockwise order
                List<ushort> indices = new List<ushort>();
                // triangles with the same normal as the plane
                indices.Add(0);
                indices.Add(3);
                indices.Add(1);
                indices.Add(3);
                indices.Add(2);
                indices.Add(1);
                // triangles with opposite normal as the plane
                indices.Add(5);
                indices.Add(6);
                indices.Add(4);
                indices.Add(6);
                indices.Add(7);
                indices.Add(4);

                // create the index buffer and set its data
                mIndexBuffer = new IndexBuffer(mGraphicsDevice, typeof(ushort), 12, BufferUsage.None);
                mIndexBuffer.SetData(indices.ToArray());
            } // end if (mDraw)
        }
        #endregion // Loading and Initialization

        #region Draw
        public void Draw(Matrix worldMatrix, Matrix viewMatrix, Matrix projectionMatrix)
        {
            // only draw this plane if the draw flag is on
            if (mDraw)
            {
                // Set BasicEffect parameters.
                mPrimitiveEffect.World = worldMatrix;
                mPrimitiveEffect.View = viewMatrix;
                mPrimitiveEffect.Projection = projectionMatrix;

                GraphicsDevice graphicsDevice = mPrimitiveEffect.GraphicsDevice;
                // save off original render states so we can reset them
                DepthStencilState origDepthStencilState = graphicsDevice.DepthStencilState;
                BlendState originalBlendState = graphicsDevice.BlendState;

                // Set renderstates.              
                graphicsDevice.DepthStencilState = DepthStencilState.Default;
                graphicsDevice.BlendState = BlendState.Opaque;

                // Set our vertex buffer and index buffer.
                graphicsDevice.SetVertexBuffer(mVertexBuffer);
                graphicsDevice.Indices = mIndexBuffer;

                foreach (EffectPass effectPass in mPrimitiveEffect.CurrentTechnique.Passes)
                {
                    effectPass.Apply();
                    // draw our 4 triangles (using our 8 vertices)
                    graphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, 8, 0, 4);
                }

                // now that we're done, reset the graphics device's render state
                graphicsDevice.DepthStencilState = origDepthStencilState;
                graphicsDevice.BlendState = originalBlendState;
            }
        }
        #endregion // Draw
    }        
}
